/* passed
 *  linux/mm/memory.c
 *
 *  (C) 1991  Linus Torvalds
 */
//#include <set_seg.h>

/*
 * 需求加载是从01.12.91 开始编写的- 在程序编制表中似乎是最重要的程序，
 * 并且应该是很容易编制的- linus
 */

/*
 * OK，需求加载是比较容易编写的，而共享页面却需要有点技巧。共享页面程序是
 * 02.12.91 开始编写的，好象能够工作- Linus。
 *
 * 通过执行大约30 个/bin/sh 对共享操作进行了测试：在老内核当中需要占用多于
 * 6M 的内存，而目前却不用。现在看来工作得很好。
 *
 * 对"invalidate()"函数也进行了修正- 在这方面我还做的不够。
 */

// 信号头文件。定义信号符号常量，信号结构以及信号操作函数原型。
#include <signal.h>

// 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。
#include <asm/system.h>

// 调度程序头文件，定义了任务结构task_struct、初始任务0 的数据，
// 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
#include <linux/sched.h>
// head 头文件，定义了段描述符的简单结构，和几个选择符常量。
#include <linux/head.h>
// 内核头文件。含有一些内核常用函数的原形定义。
#include <linux/kernel.h>

void do_exit(long code);// 进程退出处理函数，在kernel/exit.c。

//// 显示内存已用完出错信息，并退出。
static inline void oom(void)
{
	printk("out of memory\n\r");
	do_exit(SIGSEGV);// do_exit()应该使用退出代码，这里用了信号值SIGSEGV(11)
						// 相同值的出错码含义是“资源暂时不可用”，正好同义。
}

// 刷新页变换高速缓冲宏函数。
// 为了提高地址转换的效率，CPU 将最近使用的页表数据存放在芯片中高速缓冲中。
// 在修改过页表信息之后，就需要刷新该缓冲区。这里使用重新加载页目录基址
// 寄存器cr3 的方法来进行刷新。下面eax = 0，是页目录的基址。
// 重新设置cr3，以达到重新加载页表的作用！！！
#define invalidate() \
__asm__ __volatile__("movl %%eax,%%cr3"::"a" (0))

/* 下面定义若需要改动，则需要与head.s 等文件中的相关信息一起改变 */
// linux 0.11 内核默认支持的最大内存容量是16M，可以修改这些定义以适合更多的内存。
#define LOW_MEM 0x100000	// 内存低端（1MB）。
#define PAGING_MEMORY (15*1024*1024)// 分页内存15MB。主内存区最多15M。
#define PAGING_PAGES (PAGING_MEMORY>>12)// 分页后的物理内存页数。
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)// 指定物理内存地址映射为页号。
#define USED 100// 页面被占用标志。

// 该宏用于判断给定地址是否位于当前进程的代码段中。
#define CODE_SPACE(addr) \
((((addr)+4095)&~4095) < current->start_code + current->end_code)

static unsigned long HIGH_MEMORY = 0;// 全局变量，存放实际物理内存最高端地址。

// 物理内存拷贝：拷贝一页
// 复制1 页内存（4K 字节）。
#define copy_page(from,to) \
__asm__ __volatile__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))

// 用于管理1MB以上存储空间分配的记录表。每个物理页对应于一个表项，其中记录了当前被引用的次数
static unsigned char mem_map [ PAGING_PAGES ] = {0,};

/*
 * 获取首个(实际上是最后1 个:-)物理空闲页面，并标记为已使用。如果没有空闲页面，
 * 就返回0。
 */
//// 取物理空闲页面。如果已经没有可用内存了，则返回0。
// 输入：%1(ax=0) - 0；%2(LOW_MEM)；%3(cx=PAGING PAGES)；%4(edi=mem_map+PAGING_PAGES-1)。
// 输出：返回%0(ax=页面起始地址)。
// 上面%4 寄存器实际指向mem_map[]内存字节图的最后一个字节。本函数从字节图末端开始向前扫描
// 所有页面标志（页面总数为PAGING_PAGES），若有页面空闲（其内存映像字节为0）则返回页面地址。
// 注意！本函数只是指出在主内存区的一页空闲页面，但并没有映射到某个进程的线性地址去。后面

// 从物理内存页的末端往前搜索，找到一个空闲的物理页并返回
// 以下出于效率的问题，使用了内联汇编，相比用C代码去做，这种执行速度很快
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

// repne   scas byte ptr es:[edi]  
// 扫描字符串ES:[EDI]中的AL字节值，遇到AL值停止
// al被设置成0，edi被设置成mem_map的最后一项
__asm__ __volatile__(
	// while (*edi != al(0)); 
	"std ; repne ; scasb\n\t" 	// 快速找为0的项
	// if (*edi - al != 0) {
	//    *edi = 1;
	//    addr = edi << 12;	
	//} 
	"jne 1f\n\t"			// 没有等于0的项结束返回。此时al仍然0，所以返回0
	"movb $1,1(%%edi)\n\t"	// 将找到的项设定值为1, 标记为占用状态
	"sall $12,%%ecx\n\t"	// 将当前的索引左移12位，即乘以4096字节
	"addl %2,%%ecx\n\t"		// 加上LOW_MEM，这样得到了实际的物理地址

	//分配得到页面后，将该页面进行清0
	// for (i = 4092; i; i--) addr[i] = 0;
	"movl %%ecx,%%edx\n\t"	// 将页面地址给edx
	"movl $1024,%%ecx\n\t"	// 进行1024次
	"leal 4092(%%edx),%%edi\n\t"  // 定位到页面的末尾给到edi
	"rep ; stosl\n\t"			// 进行清0

	"movl %%edx,%%eax\n"		// 再取页面的起始地址
	"1: cld"
	:"=a" (__res)
	:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
	"D" (mem_map+PAGING_PAGES-1)		// edi：指向mem_map最后一项
	);

	// 返回物理页地址，当没有找到空闲项时，返回0
return __res;
}

/*
 * 释放物理地址'addr'开始的一页内存。用于函数'free_page_tables()'。
 */
//// 释放物理地址addr 开始的一页面内存。
// 1MB 以下的内存空间用于内核程序和缓冲，不作为分配页面的内存空间。
void free_page(unsigned long addr)
{
	// 必须是1MB以上的物理内存空间
	if (addr < LOW_MEM) return;

	// 不得超过物理内存末端
	if (addr >= HIGH_MEMORY)
		panic("trying to free nonexistent page");

	// 转换为索引,即页面号
	// 物理地址减去低端内存位置，再除以4KB，得页面号。
	addr -= LOW_MEM;
	addr >>= 12;

	// 计数减1，减到0自然就是释放了
	if (mem_map[addr]--) return;

	// 运行到这里是因为上面的值为0，减1后变成0xff，下面再恢复成0
	// 正常情况是不应当运行这里的，因为不能对已经空闲的页再进行释放
	mem_map[addr]=0;
	panic("trying to free free page");
}

/*
 * 下面函数释放页表连续的内存块，'exit()'需要该函数。与copy_page_tables()
 * 类似，该函数仅处理4Mb 的内存块。
 */
//// 根据指定的线性地址和限长（页表个数），释放对应内存页表所指定的内存块并置表项空闲。
// 页目录位于物理地址0 开始处，共1024 项，占4K 字节。每个目录项指定一个页表。
// 页表从物理地址0x1000 处开始（紧接着目录空间），每个页表有1024 项，也占4K 内存。
// 每个页表项对应一页物理内存（4K）。目录项和页表项的大小均为4 个字节。
// 参数：from - 起始基地址；size - 释放的长度。
// 释放指定线性地址空间区域中对应的内存，需要操作页表，并释放物理页
// 用于进程的状态和回收
// 回收时要释放物理页，页表、页目录项
int free_page_tables(unsigned long from,unsigned long size)
{
	unsigned long *pg_table;
	unsigned long * dir, nr;

	// 一个页表可映射4MB的虚拟内存空间，虚拟内存的地址是4MB对齐的
	// 对齐到4MB为单位的地址，不对齐的地址，提示出错
	if (from & 0x3fffff)
		panic("free_page_tables called with wrong alignment");

	// 从0地址开始释放4MB，显示不行，会在页目录表中将内核部分的映射给释放掉
	// 系统将无法运行
	if (!from)
		panic("Trying to free up swapper memory space");
	
	// 向上对齐到4MB，即类似3.7MB这种，通过size（字节量） + (4MB-1)/4MB = size 4MB
	// 由于每个页目录项负责4MB的映射，因此size即要释放的页目录项的项数
	size = (size + 0x3fffff) >> 22;

	// 页目录在0地址处，因此from >> 22 << 2，即右移得到的目录项的指针。0xffc是为了将低2位清0.
	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0，即除以4MB，然后清0低2位，因为一个目录表项大小为4B */

	// 遍历size个目录项
	for ( ; size-->0 ; dir++) {
		// 如果页目录项不存在，则跳过
		if (!(1 & *dir))
			continue;
		
		// 取出页表第一个表项的地址, 4KB对齐，低12位为一些标志位，去掉
		// 后面以为这个表项的地址，逐个清空
		pg_table = (unsigned long *) (0xfffff000 & *dir);

		// 遍历页表的所有项
		for (nr=0 ; nr<1024 ; nr++) {
			// 如果有映射关系，则取出物理页地址,，低12位为一些标志位，去掉。然后释放掉
			if (1 & *pg_table)
				free_page(0xfffff000 & *pg_table);
			
			// 清空表项，设置为0，这里似乎没什么必要！
			*pg_table = 0;

			// 继续处理下一项
			pg_table++;
		}

		// 再释放页表，然后将页目录项清0
		free_page(0xfffff000 & *dir);
		*dir = 0;
	}

	// 页表有更改，重新加载
	invalidate();
	return 0;
}

/*
 * 好了，下面是内存管理mm 中最为复杂的程序之一。它通过只复制内存页面
 * 来拷贝一定范围内线性地址中的内容。希望代码中没有错误，因为我不想
 * 再调试这块代码了 :-)
 *
 * 注意！我们并不是仅复制任何内存块- 内存块的地址需要是4Mb 的倍数（正好
 * 一个页目录项对应的内存大小），因为这样处理可使函数很简单。不管怎样，
 * 它仅被fork()使用（fork.c）
 *
 * 注意!!当from==0 时，是在为第一次fork()调用复制内核空间。此时我们
 * 不想复制整个页目录项对应的内存，因为这样做会导致内存严重的浪费- 我们
 * 只复制头160 个页面- 对应640kB。即使是复制这些页面也已经超出我们的需求，
 * 但这不会占用更多的内存- 在低1Mb 内存范围内我们不执行写时复制操作，所以
 * 这些页面可以与内核共享。因此这是nr=xxxx 的特殊情况（nr 在程序中指页面数）。
 */
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

	// from to必须4MB对齐
	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");

	// 计算源目录和目的目录项的起始指针，以及多少个4MB大小，也就是目录项的数量
	// 整个系统中所有进程共用同一个页表，且位于地址0处。因此from >> 20即能得到页目录项的指针
	from_dir = (unsigned long *) ((from>>20) & 0xffc);  // 源进程的from对应的页目录项指针
	to_dir = (unsigned long *) ((to>>20) & 0xffc);   // 目标fto对应的页目录项指针
	size = ((unsigned) (size+0x3fffff)) >> 22;		// 对齐到4MB大小, 实现整块的复制

	// 循环，拷贝size个目录项的配置
	// 整个循环中，只为目的地址的页表分配4KB的页表，并不分配物理内存，而只是做到物理内存的共享
	for( ; size-->0 ; from_dir++,to_dir++) {
		// 目的目录项已经存在，不应该复制，也不应该发生这种情况，死机
		if (1 & *to_dir)
			panic("copy_page_tables: already exist");
		
		// 如果源不存在，比如进程的某4MB空间中确实没有使用完，即有可能有空的项
		if (!(1 & *from_dir))
			continue;
		
		// 下面逐个开始拷贝4KB的页表了。
		// 取源页表的地址, 再为目的地址创建一个新页表,此时新分配的空间内容有清0
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
		if (!(to_page_table = (unsigned long *) get_free_page()))   // 目的页表可能不存在，分配之
			return -1;	/* Out of memory, see freeing */
		// 将新页表加入目的页表中
		*to_dir = ((unsigned long) to_page_table) | 7;		// P, RW、US

		// 如果为0，即复制进程0的160页，即640KB。否则，4MB空间
		// from=0表示从进程0开始复制，进程0的代码和数据空间不大，不超过640KB
		nr = (from==0)?0xA0:1024;

		// 循环复制
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			// 取页表项值，包含了物理地址和属性位
			this_page = *from_page_table;
			if (!(1 & this_page))
				continue;
			
			// 清0目标内存中映射的物理页的RW位，设置成只读模式，因为实现的是共享，暂时不允许写
			// 后面进程写的时候会产生写保护异常，到时就再为该进程分配物理页
			// 两个映表指向的是同一物理页
			this_page &= ~2;
			*to_page_table = this_page;			// 指向同一个物理页，设置成只读模式
			if (this_page > LOW_MEM) {
				// 超过1MB时，更新from，也会被设置成只读
				// 这样from和to两块区域都被设置成只读了
				// 后面当进程尝试写的时候，就会触发异常，在异常中再解决这个问题
				*from_page_table = this_page;

				// 增加物理页的引用计数，因为现在两个页表中都映射到了同一物理页
				// 相当于是两块进行了共享。
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}

	// 页表有新增，刷新页映射的缓存
	invalidate();
	return 0;
}

/*
 * 下面函数将一内存页面放置在指定地址处。它返回页面的物理地址，如果
 * 内存不够(在访问页表或页面时)，则返回0。
 */
// 将线性地址address和物理页page之间建立映射关系
// page是物理地址，address是虚拟地址
unsigned long put_page(unsigned long page,unsigned long address)
{
	unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */
	// 检查物理页的大小，不能超过有效的范围，其中1MB以下已经建立了映射关系
	// 检查出现问题，提示一下
	if (page < LOW_MEM || page >= HIGH_MEMORY)
		printk("Trying to put page %p at %p\n",page,address);

	// 所在的物理页应当是已经分配出去了。如果不是，打印信息提示。
	// 因为此处完成的建立映射关系，所以肯定要是已经分配的	
	if (mem_map[(page-LOW_MEM)>>12] != 1)
		printk("mem_map disagrees with %p at %p\n",page,address);

	// address对应的页目录项的地址，高12位地址
	page_table = (unsigned long *) ((address>>20) & 0xffc);

	if ((*page_table)&1)
		// 页目录项有效
		page_table = (unsigned long *) (0xfffff000 & *page_table);	// 取页表地址，低12位用于属性，屏蔽掉
	else {
		// 没有有效的页表，则申请分配一个页表
		if (!(tmp=get_free_page()))
			return 0;
		*page_table = tmp|7;	// 设置P、RW、US位，以及页表地址
		page_table = (unsigned long *) tmp;		// 获得新页表
	}

	// 将物理页写入, 4KB对齐，设置P、RW、US位，即用户可读写
	page_table[(address>>12) & 0x3ff] = page | 7;
/* no need for invalidate */

	// 在缺页时会调用，无需重新加载
	return page;
}

// 取消页表项的只读设置，给其分配一页新的物理内存
// 并非的所有共享物理页的进程页表都会进行分配，而是谁写造成的这个原因，就给谁新分配页表
void un_wp_page(unsigned long * table_entry)
{
	unsigned long old_page,new_page;

	// 取发生写保护异常的物理页地址
	old_page = 0xfffff000 & *table_entry;

	// 如果是1MB以上，即非内核进程0，并且引用计数为1，即并未共享，改为可写后退出
	if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
		*table_entry |= 2;		// 设置可写标志，刷新内存即可
		invalidate();
		return;
	}

	// 被引用了多次，新申请一页物理内容
	if (!(new_page=get_free_page()))
		oom();		// 发生错误

	// 如果大于1MB以上空间，即共享？map > 1
	// 将引用计数减1，
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;

	// 将出问题的表项设置为P、RW、US模式
	*table_entry = new_page | 7;
	invalidate();			// 刷新页表

	// 拷贝内存，即在物理页之间进行拷贝memcpy(new, old, page_size)
	copy_page(old_page,new_page);
}	

/*
 * 当用户试图往一个共享页面上写时，该函数处理已存在的内存页面，（写时复制）
 * 它是通过将页面复制到一个新地址上并递减原页面的共享页面计数值实现的。
 *
 * 如果它在代码空间，我们就以段错误信息退出。
 */
//// 页异常中断处理调用的C 函数。写共享页面处理函数。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生，address 是页面线性地址。
// 写共享页面时，需复制页面（写时复制）。
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
	if (CODE_SPACE(address))
		do_exit(SIGSEGV);
#endif
	// 错误码未使用, 取页表项地址，解除保护，然后加以修复
	// 处理取消页面保护。参数指定页面在页表中的页表项指针，其计算方法是：
	// ((address>>10) & 0xffc)：计算指定地址的页面在页表中的偏移地址；
	// (0xfffff000 &((address>>20) &0xffc))：取目录项中页表的地址值，
	// 其中((address>>20) &0xffc)计算页面所在页表的目录项指针；
	// 两者相加即得指定地址对应页面的页表项指针。这里对共享的页面进行复制。
	un_wp_page((unsigned long *)(((address>>10) & 0xffc)			// 页表中的字节偏移 >> 12 << 2
		// 获取页表地址  >> 22 << 2，取出地址后，再将低位部分的标志给清除掉，非地址的一部分
		 + (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)))));	

}

// 验证指定的线性地址是否可写
void write_verify(unsigned long address)
{
	unsigned long page;

	// 获取进程的页目录表项 >> 22 << 2，并检查是否存在
	// 如果页不存，那么也就不存在共享，因此不需要进行是否可写的检验以及调整
	if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
		return;
	
	// 取页表项地址， >> 12 << 2
	page &= 0xfffff000;
	page += ((address>>10) & 0xffc);

	// 检查页表项存在以及只读位。如果是的话，则进行写验证保护
	if ((3 & *(unsigned long *) page) == 1)  /* non-writeable, present */
		un_wp_page((unsigned long *) page);
	return;
}

// 为指定的线性地址分配空闲的物理页
void get_empty_page(unsigned long address)
{
	unsigned long tmp;

	// 申请物理页，并且将物理页与线性地址绑定在一起
	if (!(tmp=get_free_page()) || !put_page(tmp,address)) {
		// 申请失败，或者建立映射失败，则回收内存，进程退出
		free_page(tmp);		/* 0 is ok - ignored */
		oom();
	}
}

/*
 * try_to_share()在任务"p"中检查位于地址"address"处的页面，看页面是否存在，是否干净。
 * 如果是干净的话，就与当前任务共享。
 *
 * 注意！这里我们已假定p !=当前任务，并且它们共享同一个执行程序。
 */
//// 尝试对进程指定地址处的页面进行共享操作。
// 同时还验证指定的地址处是否已经申请了页面，若是则出错，死机。
// 返回1-成功，0-失败。
static int try_to_share(unsigned long address, struct task_struct * p)
{
	unsigned long from;
	unsigned long to;
	unsigned long from_page;
	unsigned long to_page;
	unsigned long phys_addr;

// 求指定内存地址的页目录项。
	from_page = to_page = ((address>>20) & 0xffc);
// 计算进程p 的代码起始地址所对应的页目录项。
	from_page += ((p->start_code>>20) & 0xffc);
// 计算当前进程中代码起始地址所对应的页目录项。
	to_page += ((current->start_code>>20) & 0xffc);
/* 在from 处是否存在页目录？ */
// *** 对p 进程页面进行操作。
// 取页目录项内容。如果该目录项无效(P=0)，则返回。否则取该目录项对应页表地址-> from。
	from = *(unsigned long *) from_page;
	if (!(from & 1))
		return 0;
	from &= 0xfffff000;
// 计算地址对应的页表项指针值，并取出该页表项内容 -> phys_addr。
	from_page = from + ((address>>10) & 0xffc);
	phys_addr = *(unsigned long *) from_page;
/* 页面干净并且存在吗？ */
// 0x41 对应页表项中的Dirty 和Present 标志。如果页面不干净或无效则返回。
	if ((phys_addr & 0x41) != 0x01)
		return 0;
// 取页面的地址 -> phys_addr。如果该页面地址不存在或小于内存低端(1M)也返回退出。
	phys_addr &= 0xfffff000;
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;
// *** 对当前进程页面进行操作。
// 取页目录项内容 -> to。如果该目录项无效(P=0)，则取空闲页面，并更新to_page 所指的目录项。
	to = *(unsigned long *) to_page;
	if (!(to & 1))
		if (to = get_free_page())
			*(unsigned long *) to_page = to | 7;
		else
			oom();
// 取对应页表地址 -> to，页表项地址 to_page。如果对应的页面已经存在，则出错，死机。
	to &= 0xfffff000;
	to_page = to + ((address>>10) & 0xffc);
	if (1 & *(unsigned long *) to_page)
		panic("try_to_share: to_page already exists");
/* 对它们进行共享处理：写保护 */
// 对p 进程中页面置写保护标志(置R/W=0 只读)。并且当前进程中的对应页表项指向它。
	*(unsigned long *) from_page &= ~2;
	*(unsigned long *) to_page = *(unsigned long *) from_page;
	// 刷新页变换高速缓冲。
	invalidate();
	// 计算所操作页面的页面号，并将对应页面映射数组项中的引用递增1。
	phys_addr -= LOW_MEM;
	phys_addr >>= 12;
	mem_map[phys_addr]++;
	return 1;
}

/*
 * share_page()试图找到一个进程，它可以与当前进程共享页面。参数address 是
 * 当前数据空间中期望共享的某页面地址。
 *
 * 首先我们通过检测executable->i_count 来查证是否可行。如果有其它任务已共享
 * 该inode，则它应该大于1。
 */
//// 共享页面。在缺页处理时看看能否共享页面
// 返回1 - 成功，0 - 失败。。
static int share_page(unsigned long address)
{
	struct task_struct ** p;

// 如果是不可执行的，则返回。excutable 是执行进程的内存i 节点结构。
	if (!current->executable)
		return 0;
// 如果只能单独执行(executable->i_count=1)，也退出。
	if (current->executable->i_count < 2)
		return 0;
// 搜索任务数组中所有任务。寻找与当前进程可共享页面的进程，
// 并尝试对指定地址的页面进行共享。
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)// 如果该任务项空闲，则继续寻找。
			continue;
		if (current == *p)// 如果就是当前任务，也继续寻找。
			continue;
		if ((*p)->executable != current->executable)// 如果executable 不等，也继续。
			continue;
		if (try_to_share(address,*p))// 尝试共享页面。
			return 1;
	}
	return 0;
}

//// 页异常中断处理调用的函数。处理缺页异常情况。在page.s 程序中被调用。
// 参数error_code 是由CPU 自动产生，address 是页面线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

	address &= 0xfffff000;// 页面地址。
// 首先算出指定线性地址在进程空间中相对于进程基址的偏移长度值。
	tmp = address - current->start_code;
// 若当前进程的executable 空，或者指定地址超出代码+数据长度，则申请一页物理内存，并映射
// 影射到指定的线性地址处。executable 是进程的i 节点结构。该值为0，表明进程刚开始设置，
// 需要内存；而指定的线性地址超出代码加数据长度，表明进程在申请新的内存空间，也需要给予。
// 因此就直接调用get_empty_page()函数，申请一页物理内存并映射到指定线性地址处即可。
// start_code 是进程代码段地址，end_data 是代码加数据长度。对于linux 内核，它的代码段和
// 数据段是起始基址是相同的。
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);
		return;
	}
// 如果尝试共享页面成功，则退出。
	if (share_page(tmp))
		return;
// 取空闲页面，如果内存不够了，则显示内存不够，终止进程。
	if (!(page = get_free_page()))
		oom();
/* 记住，（程序）头要使用1 个数据块 */
// 首先计算缺页所在的数据块项。BLOCK_SIZE = 1024 字节，因此一页内存需要4 个数据块。
	block = 1 + tmp/BLOCK_SIZE;
// 根据i 节点信息，取数据块在设备上的对应的逻辑块号。
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);
// 读设备上一个页面的数据（4 个逻辑块）到指定物理地址page 处。
	bread_page(page,current->executable->i_dev,nr);
// 在增加了一页内存后，该页内存的部分可能会超过进程的end_data 位置。下面的循环即是对物理
// 页面超出的部分进行清零处理。
	i = tmp + 4096 - current->end_data;
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
// 如果把物理页面映射到指定线性地址的操作成功，就返回。否则就释放内存页，显示内存不够。
	if (put_page(page,address))
		return;
	free_page(page);
	oom();
}

//// 物理内存初始化。
// 参数：start_mem - 可用作分页处理的物理内存起始位置（已去除RAMDISK 所占内存空间等）。
// end_mem - 实际物理内存最大地址。
// 在该版的linux 内核中，最多能使用16Mb 的内存，大于16Mb 的内存将不于考虑，弃置不用。
// 0 - 1Mb 内存空间用于内核系统（其实是0-640Kb）。
void mem_init(long start_mem, long end_mem)
{
	int i;

	HIGH_MEMORY = end_mem;

	// 将1MB以上的所有物理页设置为使用状态
	// 这个地址可能是1MB，也可能是2MB等，根据实际高速缓存和ramdisk的配置情况来决定
	for (i=0 ; i<PAGING_PAGES ; i++)
		mem_map[i] = USED;

	// 计算主页起始的 在mem_map的索引号
	i = MAP_NR(start_mem);

	// 计算主存区域整个有多少个物理页
	end_mem -= start_mem;
	end_mem >>= 12;		// 4KB页

	// 不断循环减递，将主存区域所有的物理页设置为空闲状态，方便日后分配
	while (end_mem-->0)
		mem_map[i++]=0;
}

// 显示内存映射信息等，用于统计调试
// 功能为：统计空闲物理页的数量；详细统计每个页目录项中所使用的页数量
// 可以自己再加点发挥，都可以
void calc_mem(void)
{
	int i,j,k,free=0;
	long * pg_tbl;

	// 计算主存中有多少个空闲的物理页，然后将这些信息给打印出来
	for(i=0 ; i<PAGING_PAGES ; i++)
		if (!mem_map[i]) free++;		// map[i] == 0，即没有引用
	printk("%d pages free (of %d)\n\r",free,PAGING_PAGES);

	// 扫描页目录表，统计所有各目录项中使用多少物理页
	// 这里写的是2，但linux 0.12中写的是4，即0-3表项在head.s中已经被配置为内核使用
	// 所以应当用4，统计以上的地址
	for(i=2 ; i<1024 ; i++) {
		if (1&pg_dir[i]) {		// 看看是否有效
			// 取页表物理地址
			pg_tbl=(long *) (0xfffff000 & pg_dir[i]);
			for(j=k=0 ; j<1024 ; j++)
				if (pg_tbl[j]&1)		// 如果有效，即有映射关系，表明使用了一个物理页
					k++;
			// 打印当前物理页的使用情况
			printk("Pg-dir[%d] uses %d pages\n",i,k);
		}
	}
}
